package digital_cash_system.customer;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.net.Socket;
import java.util.ArrayList;

import digital_cash_system.Coin;
import digital_cash_system.Logger;
import digital_cash_system.PublishedHash;
import digital_cash_system.Utils;
import digital_cash_system.bank.BankBase;
import digital_cash_system.bank.Publishedinfo;
import digital_cash_system.shop.ShopBase;

public class Customer
{
	private BigInteger p;
	private BigInteger q;
	private BigInteger g;
	private BigInteger g1;
	private BigInteger g2;
	private BigInteger h;

	private BigInteger h_u;
	private BigInteger u1;

	private BigInteger z;

	private ArrayList<XCoin> coins = new ArrayList<XCoin>();

	public Customer()
	{
	}

	public void openAccount()
	{
		try
		{
			Socket clientSocket = new Socket(BankBase.BankAddr, BankBase.Port);
			ObjectOutputStream outputStream = new ObjectOutputStream(clientSocket.getOutputStream());
			// --- send request type
			outputStream.writeObject(BankBase.RequestType.OpenAccount);
			outputStream.flush();
			// ---

			ObjectInputStream inputStream = new ObjectInputStream(clientSocket.getInputStream());
			// --- read published parms
			Publishedinfo publishedinfo = (Publishedinfo) inputStream.readObject();
			p = publishedinfo.getP();
			q = publishedinfo.getQ();
			g = publishedinfo.getG();
			g1 = publishedinfo.getG1();
			g2 = publishedinfo.getG2();
			h = publishedinfo.getH();
			// ---

			BigInteger m;
			do
			{
				u1 = Utils.generateZ_qMember(q);
				h_u = g1.modPow(u1, p);
				m = h_u.multiply(g2).mod(p);
			} while (m.equals(BigInteger.ONE));

			// send id h_u;
			outputStream.writeObject(h_u);

			// --- Logger output
			Logger.getInstance().write("Send to Bank h_u = " + h_u);
			// ---

			// z=ReceiveSignature
			z = (BigInteger) inputStream.readObject();

			// --- Logger output
			Logger.getInstance().write("Receive from Bank z = " + z);
			// ---

			// --- Logger output
			Logger.getInstance().write("Account opened successfully");
			// ---

			inputStream.close();
			outputStream.close();
			clientSocket.close();
		} catch (IOException | ClassNotFoundException e)
		{
			e.printStackTrace();
		}
	}

	public void withdrawCoin()
	{
		try
		{
			Socket clientSocket = new Socket(BankBase.BankAddr, BankBase.Port);
			ObjectOutputStream outputStream = new ObjectOutputStream(clientSocket.getOutputStream());
			// --- send request type
			outputStream.writeObject(BankBase.RequestType.Withdraw);
			outputStream.flush();
			// ---

			ObjectInputStream inputStream = new ObjectInputStream(clientSocket.getInputStream());

			// --- --- prove account ownership
			// --- Logger output
			Logger.getInstance().write("Prove account ownership");
			// ---
			outputStream.writeObject(h_u);
			BigInteger w = Utils.generateZ_qMember(q);
			BigInteger y = g1.modPow(w, p);
			outputStream.writeObject(y);
			BigInteger c_r = (BigInteger) inputStream.readObject();
			BigInteger r_r = w.add(c_r.multiply(u1)).mod(p);
			outputStream.writeObject(r_r);
			// --- ---

			// receive a, b
			BigInteger a = (BigInteger) inputStream.readObject();
			BigInteger b = (BigInteger) inputStream.readObject();

			// --- Logger output
			Logger.getInstance().write("Receive from Bank a = " + a);
			// ---

			// --- Logger output
			Logger.getInstance().write("Receive from Bank b = " + b);
			// ---

			BigInteger s = Utils.generateZ_qMember(q);
			BigInteger x1 = Utils.generateZ_qMember(q);
			BigInteger x2 = Utils.generateZ_qMember(q);

			BigInteger A = h_u.multiply(g2).modPow(s, p);
			BigInteger B = g1.modPow(x1, p).multiply(g2.modPow(x2, p)).mod(p);
			BigInteger z_ = z.modPow(s, p);

			BigInteger u = Utils.generateZ_qMember(q);
			BigInteger v = Utils.generateZ_qMember(q);

			BigInteger a_ = a.modPow(u, p).multiply(g.modPow(v, p)).mod(p);
			BigInteger b_ = b.modPow(s.multiply(u), p).multiply(A.modPow(v, p)).mod(p);

			BigInteger c_ = PublishedHash.hashH(A, B, z_, a_, b_, q);

			BigInteger c = c_.multiply(u.modInverse(q)).mod(q);

			// send c
			outputStream.writeObject(c);
			// --- Logger output
			Logger.getInstance().write("Send to Bank c = " + c);
			// ---

			// receive r
			BigInteger r = (BigInteger) inputStream.readObject();

			// --- Logger output
			Logger.getInstance().write("Receive from Bank r = " + r);
			// ---

			boolean accept1 = g.modPow(r, p).equals(a.multiply(h.modPow(c, p)).mod(p));
			boolean accept2 = h_u.multiply(g2).modPow(r, p)
					.equals(b.multiply(z.modPow(c, p)).mod(p));

			if (accept1 && accept2)
			{
				// --- --- accept coin
				// --- Logger output
				Logger.getInstance().write("Coin withdrawn successfully");
				// ---
				BigInteger r_ = v.add(r.multiply(u)).mod(q);
				boolean acceptCoin2 = g.modPow(r_, p).equals(a_.multiply(h.modPow(c_, p)).mod(p));
				coins.add(new XCoin(new Coin(A, B, z_, a_, b_, r_), s, x1, x2, c.multiply(u)));
				// --- Logger output
				Logger.getInstance().write("Current coins count: " + coins.size());
				// ---
				// --- ---
			} else
			{
				// TODO do not accept coin
				// --- Logger output
				Logger.getInstance().write("Coin not accepted");
				Logger.getInstance().write("Withdraw failed");
				// ---
			}

			inputStream.close();
			outputStream.close();
			clientSocket.close();
		} catch (IOException | ClassNotFoundException e)
		{
			e.printStackTrace();
		}
	}

	public void pay()
	{
		try
		{
			Socket clientSocket = new Socket(ShopBase.ShopAddr, ShopBase.Port);
			ObjectOutputStream outputStream;
			outputStream = new ObjectOutputStream(clientSocket.getOutputStream());
			// --- send request type
			outputStream.writeObject(ShopBase.RequestType.Pay);
			outputStream.flush();
			// ---

			ObjectInputStream inputStream = new ObjectInputStream(clientSocket.getInputStream());

			// TODO get coin
			XCoin xCoin = coins.remove(0);
			// --- Logger output
			Logger.getInstance().write("Coin obtained");
			// ---

			// send coin
			outputStream.writeObject(xCoin.getCoin());

			// receive d
			BigInteger d = (BigInteger) inputStream.readObject();

			// --- Logger output
			Logger.getInstance().write("Receive from Shop d = " + d);
			// ---

			BigInteger r1 = d.multiply(u1.multiply(xCoin.getS())).add(xCoin.getX1()).mod(q);
			BigInteger r2 = d.multiply(xCoin.getS()).add(xCoin.getX2()).mod(q);

			// send to the shop r1 r2
			outputStream.writeObject(r1);
			outputStream.writeObject(r2);

			// --- Logger output
			Logger.getInstance().write("Send to Shop r1 = " + r1);
			// ---
			
			// --- Logger output
			Logger.getInstance().write("Send to Shop r2 = " + r2);
			// ---

			// --- Logger output
			Logger.getInstance().write("Payment completed successfully");
			// ---

			inputStream.close();
			outputStream.close();
			clientSocket.close();
		} catch (IOException | ClassNotFoundException e)
		{
			e.printStackTrace();
		}
	}

}

class XCoin
{
	private Coin coin;
	private BigInteger s;
	private BigInteger x1;
	private BigInteger x2;
	private BigInteger c_2;

	public XCoin(Coin coin, BigInteger s, BigInteger x1, BigInteger x2, BigInteger c_2)
	{
		super();
		this.coin = coin;
		this.s = s;
		this.x1 = x1;
		this.x2 = x2;
		this.c_2 = c_2;
	}

	public Coin getCoin()
	{
		return coin;
	}

	public BigInteger getS()
	{
		return s;
	}

	public BigInteger getX1()
	{
		return x1;
	}

	public BigInteger getX2()
	{
		return x2;
	}

	public BigInteger getC_2()
	{
		return c_2;
	}
}
